import requests
from urllib import parse
import csv
import pandas as pd
import os
from pathlib import Path
import time
from time import sleep
import re
import random

def save_csv(file_name,write,encoding="utf-8-sig"):
    # encoding = "utf-8-sig" 用于解决中文写入csv乱码问题
    #！！！！能别用gbk就别用gbk坑太多！！！
    #write之前老师给的例子中传入的是元组，list也行，a是不断写入，w是清除写
    #gbk无法转换'\xa0'之类字符,所以在转换前需要将“\”替换掉，不过！！！没法完全替换！！！，很多是不可见的
    # repr() 函数可以将字符串转换为python的原始字符串（即忽视各种特殊字符的作用)不然replace不了'//'
    #再用eval()还原，不然写入后的文字都是带引号的
    if encoding == 'gbk':
        new = []
        for info in write:
            if type(info) == str:#里面有int或者其他格式，需要判断一下是否是str再替换
                info = eval(repr(info).replace(u'\\', u'/'))
            new.append(info)
        write = new

    with open(file_name, "a",newline='', encoding=encoding) as file:
        csv_writer = csv.writer(file)
        csv_writer.writerow(write)

def header_trans(header_filename,prin = False):
    '''header直接复制粘贴到txt中，header_filename（如：'headers.txt'）'''
    with open(header_filename, "r") as f:  # 打开文件
        headers = f.read()  # 读取文件
    #按\n分割成list
    trans_list = headers.split('\n')

    #将list转换成dict
    trans_dict = {}
    for i in trans_list:
        if ': ' in i:
            item = i.split(': ')
        elif ':' in i:
            item = i.split(':')
        else:
            print("未找到分隔符':'or': '")
        trans_dict[item[0]]=item[1]
    if prin == True:
        print(trans_dict)

    return trans_dict

class KuWoZhangYi:
    #header，cooki，url，路径都在这里
    def __init__(self,keyword='周杰伦',num=10):
        '''
        :param keyword: str,如'周杰棍'
        :param num: int，控制获取列表数量，最大150
        '''
        self.keyword = parse.quote(keyword)
        self.folder = "酷我歌曲列表"
        self.search_url = "http://www.kuwo.cn/search/list?key="+ self.keyword
        self.download_url = 'https://www.kuwo.cn/url?format=mp3&rid={}&type=convert_url3'
        #获取歌曲列表的页面，rn= 可以决定每月获取的数量，200就返回504的错误了，就姑且150吧，懒得再试了
        #本来后面加个pg = ？就到列表的第几页了，但是改了之后发现没用，本来后面还有一串，发现删了也没事
        self.search_url_ajax = "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key={}".format(self.keyword)+'pn=1&rn={}&httpsStatus=1'.format(num)
        #在searchMusicBykeyWord?key=……那个ajax的请求头中截取的cookis，只不过把token删了
        self.cookies ='_ga=GA1.2.1322857731.1625799711; _gid=GA1.2.1019365014.1625799711; Hm_lvt_cdb524f42f0ce19b169a8071123a4797=1625799710,1625829034; channel_from_id=1001007075; userid=1217176587; websid=3251883595; liveCheck=1; Hm_lpvt_cdb524f42f0ce19b169a8071123a4797=1625830361; watched=548065693; kw_token='
        self.headers = header_trans('kuwo_headers.txt')

    # 去掉windows文件名不允许出现的[？，。“”?、,·~\/'"*“”<>|]
    def drop_win_forbid(self, s):
        p = re.compile(r'[-,$()#+&*？/，\\。“”?、·~"\*<>|]')
        s = re.split(p, s)
        return '_'.join(s)

    #根据搜索关键词进行搜索，并获取歌曲列表信息
    def get_songlist(self,csv_name,sleep_time = random.uniform(5,10)):
        #批量爬取表单会比较多，先建个文件夹
        if Path(self.folder).is_dir():pass
        else:os.mkdir(self.folder)  # 创建文件夹

        ######  获取搜索到的歌曲  ##########
        response = requests.get(self.search_url + self.keyword )#获取搜索页面
        # response.cookies先获取到cookies，是字典，所以用get key的形式获取返回的token，以应对反爬！！！
        kw_token = response.cookies.get("kw_token")
        headers = {
            "Referer": self.search_url,
            "csrf": kw_token,
            "Cookie": self.cookies + kw_token,
            "Host": "www.kuwo.cn",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
        }

        sleep(1)#先等一秒，不然服务器报错
        search_response = requests.get(self.search_url_ajax, headers=headers,timeout=3)  # 进行请求

        ######  将搜索到的歌曲保存到csv中  ######
        try:
            song_json = search_response.json()#这个字典一层套一层的慢慢解开吧
            song_data = song_json.get('data')
            #song_num = song_data.get('total') #提取总歌曲数
            #total_pn = song_num//30+1 #每页30个歌曲，这样就知道列表一共有多少页了,不过反爬换不了页暂时没用

            song_info_list = song_data.get('list') #现在提取歌曲列表

            save_name = self.folder +'\\'+csv_name #拼接文件保存路径
            save_csv(save_name, ['name', 'artist', 'album', 'rid', 'songTime']) #先给文件写个列名
            for song in song_info_list:
                #artist和name后面都要做歌曲文件名，需要剔除windows禁止出现的字符
                name = self.drop_win_forbid(song.get('name'))  # 歌名
                artist = self.drop_win_forbid(song.get('artist')) #歌手
                album = song.get('album')  # 像是专辑名
                rid = song.get('rid') #有这个就能下载了，不管是否收费
                songTime = song.get('songTimeMinutes')#时长
                #写入csv，encoding='utf-8-sig'解决utf-8写入csv乱码问题
                save_csv(save_name, [name,artist,album,rid,songTime])

            print('搜索关键字：{}，已存入{}，{}首歌曲，调用rid拼接链接即可下载'.format(parse.unquote(self.keyword),csv_name,len(song_info_list)))
            sleep(sleep_time)
        except Exception as e:
            print('T_T 搜索关键字：{} 爬取失败'.format(parse.unquote(self.keyword)))
            print(search_response,e)

    #去除文件中重复以及rid为空的行，都是inplace删的
    def drop_Nanrid_duplicates(self,data):
        data.drop_duplicates(inplace=True)
        missing_rid_index = data[data['rid'].isnull().values == True].index  # 得到rid有缺失值的行的索引，
        data.drop(missing_rid_index, inplace=True)

    # 对数据进行整理, 去掉文件中重复，rid有空值，与关键字无关的内容以及windows文件名中禁止出现的字符
    def tidy_data(self,file_name,key = True):
        data = pd.read_csv(file_name)  # 读取csv
        self.drop_Nanrid_duplicates(data)  # 去重，去掉没有rid的行
        if key == True:
            keyword = file_name.split('.')[0].split('\\')[1]  # 以文件名为关键字
            # 删除name，artist两列中没有关键字的行
            name_contain_index = data[data['name'].str.contains(keyword) == True].index
            artist_contain_index = data[data['artist'].str.contains(keyword) == True].index
            have_index = list(set(name_contain_index) | set(artist_contain_index))
            data = data.loc[have_index,:]
        # 去掉name，artist中，windows文件名不允许出现的[？，。“”?、,·~\/'"*“”<>|]
        data['name'] = data['name'].map(self.drop_win_forbid)
        data['artist'] = data['artist'].map(self.drop_win_forbid)
        return data

    # ode = 'all'是文件夹中的所有文件，excep是指定哪些文件不做更改,也可以指定只修改哪些文件
    def drop_not_need(self,mode = 'all',except_list = None,name_list = None):
        def save_data(name,data):
            writer = pd.ExcelWriter(name)
            data.to_excel(writer, index=False)
            writer.save()
            writer.close()

        if mode == 'all':
            file_list = os.listdir(self.folder)
            if except_list:
                for drop in except_list:
                    file_list.pop(drop)
            print(file_list)
            for file_name in file_list:
                print(file_name)
                data = self.tidy_data(file_name)
                save_data('tidy_' + file_name, data)
        else:
            for file_name in name_list:
                data = self.tidy_data(file_name)
                save_data('tidy_' + file_name, data)

    # 根据rid对歌曲进行下载
    def down_load(self,rid,save_name,sleep_time = random.uniform(5,10)):
        if Path(save_name).is_file():
            print('歌曲：{} 已存在，不用下载'.format(save_name))
        else:
            start = time.time()
            download_url = self.download_url.format(rid)
            try:
                response = requests.get(download_url,headers = self.headers,timeout=3)
                result_json = response.json()  # get来的是个json
                music_url = result_json["url"]  # 提取下载链接
                music_response = requests.get(music_url)

                with open(save_name, "wb") as file:
                    file.write(music_response.content)
                    file.flush()
                end = time.time()
                print('歌曲：{} 已下载完毕，用时：{:.2f} 秒'.format(save_name,end-start))

            except Exception as e:
                print('T_T 歌曲：{} 爬取失败,返回值：{},返回内容：{}'.format(save_name,response,response.text))
                print(e)
            sleep(sleep_time)

    #对csv中的歌曲进行下载
    def csv_download(self,csv_name,tydi=True,tydi_key =True  ,folder=True,sleep_time = random.uniform(5,10)):

        if folder:
            folder_path = self.folder + '\\'+csv_name.split('.')[0]
            if Path(folder_path).is_dir(): pass
            else:os.mkdir(folder_path) # 创建文件夹
        else:
            folder_path = self.folder

        csv_path = self.folder + '\\'+csv_name
        if Path(csv_path).is_file():#检验文件是否存在
            if tydi:
                data = self.tidy_data(csv_path,key= tydi_key)#对csv进行整理，剔除无用的
            else:
                data = pd.read_csv(csv_path)

            #获取歌曲信息
            rids = data['rid']
            names = data['name']
            artists = data['artist']
            for rid,name,artist in zip(rids,names,artists):
                save_name = folder_path + '\\'+ name + '_' + artist + ".mp3"#拼接文件名
                self.down_load(rid,save_name,sleep_time =sleep_time)#进行下载

        else:print('歌曲的csv文件未找到：{}'.format(csv_name))

    #对文件夹中所有csv进行歌曲下载
    def folder_download(self,mode = 'all',except_list = None,name_list = None,tydi=True,tydi_key =True,sleep_time = random.uniform(5,10) ):
        if mode == 'all':
            file_list = os.listdir(self.folder)
            if except_list:
                for drop in except_list:
                    file_list.pop(drop)
            for file_name in file_list:
                self.csv_download( self.folder + '\\'+ file_name, tydi=tydi,tydi_key =tydi_key,sleep_time =sleep_time)
        else:
            for file_name in name_list:
                self.csv_download(self.folder + '\\' + file_name, tydi=tydi,tydi_key =tydi_key,sleep_time =sleep_time)

    #歌曲搜索+下载
    def one_song_download(self):
        self.get_songlist(parse.unquote(self.keyword)+'.csv')
        self.csv_download(parse.unquote(self.keyword)+'.csv', tydi=True, tydi_key=True,folder=True)


if __name__ == '__main__':
    song_list = ['归去来兮','大鱼','逆浪千秋','大氿歌','天下局','权御天下','万神纪','九九八十一','万象霜天',
                 '踏山河','牵丝戏','锦鲤抄','孽海记','囍','人间月','易安难安','天下','千秋梦','俗世呀','处处吻',
                 '木兰行','丽海为王','白日妄想','乌合之众','孽海记','本色','琵琶行','盗将行','若当来世','青鸟',
                 'aliez','天策枪魂','风花雪月','曲话封神','我本无名','拔剑神曲','核爆神曲']
    
    artist_list = ['梁静茹','张信哲','朴树','张韶涵','许嵩','张惠妹','刘德华','米津玄师','凤凰传奇','韩红']

    have_down = ['周杰伦', '花粥', 'G.E.M.邓紫棋', '戴荃', '祖娅纳惜', '陈乐一', '肥皂菌','莫文蔚','林俊杰','五月天','信乐团']
    cant_down = ['三无','陈奕迅','许巍','汪峰','周深']

    #for song in song_list:
        #pa = KuWoZhangYi(song,7)
        #pa.get_songlist('B站歌单.csv')
    #pa = KuWoZhangYi()
    #pa.csv_download('B站歌单.csv',tydi=True,folder=True,tydi_key=False)

    for artist in artist_list:
        pa = KuWoZhangYi(artist,150)
        pa.one_song_download()




"""

待解决问题：一部分歌曲列表能爬到，一部分还不行

酷狗音乐搜索获取列表
    酷狗音乐每次点击搜索，抓包工具都会抓到一个叫searchKey?key=h……的东西
    在这里面respons headers里面会有一个，set_cookie:token：……每次都会变
    点击回车搜索后会抓到一个叫：searchMusicBykeyWord?key=……的东西，里面有搜索内容的列表
    这个请求的Request headers里面有一些验证机制：
        cooki中的token要与上一次respons中给出的token一致，
        并且还有csrf，是防止跨站攻击的，内容与token中也一样，
        这两个值一定要进行更新
        同时加上"Referer"，"Host"，"User-Agent":才能正常访问

    列表链接后面 key= 关键字部分采用的是编码，需要转换一下
    pn=1代表的是第一页，后面删除了也能用
    http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=%E5%8D%8A%E5%A3%B6%E7%BA%B1&pn=1&rn=30&httpsStatus=1&reqId=3f3c7210-e07a-11eb-8226-c74bd3808cf2

    from urllib import parse
    music = parse.quote(‘要搜索的内容’)
    
进行酷狗音乐的下载
    酷狗音乐详情页（搜索列表页点击播放时也会抓到）
    url?format=mp3&rid……
    在XHR分类中，Preview里面显示的是含有播放链接的字典

    从header里面获取 Request URL，get后.json()就可以拿到字典内容
    url是这样的：太长了给换了个行
        http://www.kuwo.cn/url?format=mp3&rid=171202352&response=url&type=convert_url3&br=128kmp3&
        from=web&t=1625807012355&httpsStatus=1&reqId=01f3fc41-e073-11eb-98d5-013e1e475a81

    其中很多信息是可以省略的，比如这样也可以获取到需要的信息：
        http://www.kuwo.cn/url?format=mp3&rid=171202352&type=convert_url3&br=128kmp3
        其中只要更改rid就可以获得所需的歌曲，最后的br控制音频质量

歌手爬了两个，周杰伦没问题，邓紫棋就是乱七八糟的，花粥小众的爬不出来
尝试爬的歌曲：
    搜索关键字：归去来兮，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    T_T 搜索关键字：大鱼 爬取失败
    搜索关键字：逆浪千秋，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：大氿歌，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：天下局，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：权御天下，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：万神纪，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：九九八十一，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：万象霜天，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：踏山河，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：牵丝戏，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：锦鲤抄，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：孽海记，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    T_T 搜索关键字：囍 爬取失败
    搜索关键字：人间月，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：易安难安，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    T_T 搜索关键字：天下 爬取失败
    搜索关键字：千秋梦，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：俗世呀，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：处处吻，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：木兰行，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：丽海为王，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：白日妄想，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：乌合之众，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：孽海记，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    T_T 搜索关键字：本色 爬取失败
    搜索关键字：琵琶行，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：盗将行，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：若当来世，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    T_T 搜索关键字：青鸟 爬取失败
    T_T 搜索关键字：aliez 爬取失败
    搜索关键字：天策枪魂，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：风花雪月，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：曲话封神，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载
    搜索关键字：我本无名，已存入歌单.csv，10首歌曲，调用rid拼接链接即可下载

"""